package com.trackerapp.location;

import java.util.HashMap;
import java.util.Observable;
import java.util.Observer;

import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.util.Log;

import com.trackerapp.util.MessageUtil;
import com.trackerapp.util.NetworkConnectionUtil;


public class TrackerLocationManager extends Observable {
    private final String DEBUG_TAG;

    public static final int UPDATE_CURRENT_LOCATION = 100;
    public static final float SIGNIFICANT_ACCURACY = 400.0f;

    private static final int SIGNIFICANT_TIME = 1000 * 60 * 2;
    private static final float MIN_DISTANCE = 10.0f; // Meters
    private static final long MIN_TIME = 25000L; // Milliseconds

    private static final long TIME_OUT = 50000L; // Milliseconds

    private static final boolean KEEP_LISTENING_TILL_TIMEOUT = true;

    private Context mContext;
    private LocationManager locationManager;
    private Location currentLocation;
    private String currentAddress;
    private boolean gpsLocationUpdateInProgress = false;
    private boolean networkLocationUpdateInProgress = false;
    private GpsProviderListener gpsProviderListener = new GpsProviderListener();
    private NetworkProviderListener networkProviderListener = new NetworkProviderListener();
    private GpsTimer gpsTimer;
    private NetworkTimer networkTimer;
    private boolean gpsTimerActive;
    private boolean networkTimerActive;
    private boolean foregroundStatus;
    HashMap<String, String> addressCache;

    //  #############################################################################
//  #                                 Singleton                                 #
//  #############################################################################
    private static TrackerLocationManager instance;

    public static TrackerLocationManager getInstance() {
        if (instance == null) {
            instance = new TrackerLocationManager();
        }
        return instance;
    }


    public TrackerLocationManager() {
        DEBUG_TAG = this.getClass().getSimpleName();
        addressCache = new HashMap<String, String>();
        gpsLocationUpdateInProgress = false;
        networkLocationUpdateInProgress = false;
        foregroundStatus = true;
    }

    @Override
    public void addObserver(Observer observer) {
        super.addObserver(observer);
    }


    @Override
    public void deleteObserver(Observer observer) {
        super.deleteObserver(observer);
    }


    @Override
    public void deleteObservers() {
        super.deleteObservers();
    }


    public void notifyObservers(int identifier) {
        this.setChanged();
        this.notifyObservers(new Integer(identifier));
    }

    private LocationManager getLocationManager() {
        if (locationManager == null) {
            locationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
        }
        return locationManager;
    }

    private void requestGpsLocationUpdates() {
        if (!gpsLocationUpdateInProgress) {
            System.out.println(" ######@@@Request gps location updates");
            getLocationManager().requestLocationUpdates(LocationManager.GPS_PROVIDER, MIN_TIME, MIN_DISTANCE, gpsProviderListener);
            gpsLocationUpdateInProgress = true;
            startGpsTimer();
        } else {
            System.out.println(" ######@Skip request for gps location updates as it's already running");
        }
    }

    private void requestNetworkLocationUpdates() {
        if (!networkLocationUpdateInProgress) {
            System.out.println(" ##### @@@Request network location updates  ######## ");
            getLocationManager().requestLocationUpdates(LocationManager.NETWORK_PROVIDER, MIN_TIME, MIN_DISTANCE, networkProviderListener);
            networkLocationUpdateInProgress = true;
            startNetworkTimer();
        } else {
            System.out.println(" ######@Skip request for network location updates as it's already running");
        }
    }

    public void requestLocationUpdates(Context context) {
        mContext = context;
        System.out.println(" ########## requestLocationUpdates ############# ");
        LocationManager locationManager = getLocationManager();
        boolean gpsProviderEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
        boolean networkProviderEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);

        if (networkProviderEnabled) {
            requestNetworkLocationUpdates();
        }
        if (gpsProviderEnabled) {
            requestGpsLocationUpdates();
        }
    }

    private void removeGpsLocationUpdates() {
        if (gpsLocationUpdateInProgress) {
            try {
                System.out.println(" ######Remove gps location updates");
                gpsLocationUpdateInProgress = false;
                getLocationManager().removeUpdates(gpsProviderListener);
            } catch (Exception ex) {
                
            }
        }
    }

    private void removeNetworkLocationUpdates() {
        if (networkLocationUpdateInProgress) {
            try {
                System.out.println(" ######### Remove network location updates ########## ");
                networkLocationUpdateInProgress = false;
                getLocationManager().removeUpdates(networkProviderListener);
            } catch (Exception ex) {
                Log.v(DEBUG_TAG, "Fail in Remove NetworkProvider...");
            }
        }
    }

    private void updateGpsLocation(Location newLocation) {
        System.out.println(" ######@@@Obtained gps location: " + newLocation.getLatitude() + "," + newLocation.getLongitude() + "; accuracy: " + newLocation.getAccuracy());
        sendCurrentLocation(newLocation);
        if (!KEEP_LISTENING_TILL_TIMEOUT) {
            removeGpsLocationUpdates();
        }
    }


    private void updateNetworkLocation(Location newLocation) {
        System.out.println("  ########## Obtained network location: " + newLocation.getLatitude() + "," + newLocation.getLongitude() + "; accuracy: " + newLocation.getAccuracy());
        sendCurrentLocation(newLocation);
        if (!KEEP_LISTENING_TILL_TIMEOUT) {
            removeNetworkLocationUpdates();
        }
    }

    private Location getLastKnownLocation() {
        LocationManager locationManager = getLocationManager();

        Location lastKnownLocation = locationManager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER);
        if (lastKnownLocation != null) {
            System.out.println(" ######@@@Obtained passive location: " + lastKnownLocation.getLatitude() + "," + lastKnownLocation.getLongitude() + "; accuracy: " + lastKnownLocation.getAccuracy());
        }
        Location gpsLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
        if ((gpsLocation != null) && isBetterLocation(gpsLocation, lastKnownLocation)) {
            lastKnownLocation = gpsLocation;
        }

        Location networkLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
        if ((networkLocation != null) && isBetterLocation(networkLocation, lastKnownLocation)) {
            lastKnownLocation = networkLocation;
        }

        return lastKnownLocation;
    }


    protected boolean isBetterLocation(Location location, Location currentBestLocation) {
        if (currentBestLocation == null) {
            // A new location is always better than no location
            return true;
        }

        // Check whether the new location fix is newer or older
        long timeDelta = location.getTime() - currentBestLocation.getTime();
        boolean isSignificantlyNewer = timeDelta > SIGNIFICANT_TIME;
        boolean isSignificantlyOlder = timeDelta < -SIGNIFICANT_TIME;
        boolean isNewer = timeDelta > 0;

        // If it's been more than two minutes since the current location, use the new location
        // because the user has likely moved
        if (isSignificantlyNewer) {
            return true;
            // If the new location is more than two minutes older, it must be worse
        } else if (isSignificantlyOlder) {
            return false;
        }

        // Check whether the new location fix is more or less accurate
        float accuracyDelta = location.getAccuracy() - currentBestLocation.getAccuracy();
        boolean isLessAccurate = accuracyDelta > 0.0f;
        boolean isMoreAccurate = accuracyDelta < 0.0f;
        boolean isSignificantlyLessAccurate = accuracyDelta > SIGNIFICANT_ACCURACY;

        // Check if the old and new location are from the same provider
        boolean isFromSameProvider = isSameProvider(location.getProvider(), currentBestLocation.getProvider());

        // Determine location quality using a combination of timeliness and accuracy
        if (isMoreAccurate) {
            return true;
        } else if (isNewer && !isSignificantlyLessAccurate) {
            return true;
        } else if (isNewer && !isLessAccurate) {
            return true;
        } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
            return true;
        }

        return false;
    }

    /**
     * Checks whether two providers are the same
     */
    private boolean isSameProvider(String provider1, String provider2) {
        if (provider1 == null) {
            return provider2 == null;
        }
        return provider1.equalsIgnoreCase(provider2);
    }

    synchronized private void sendCurrentLocation(Location newLocation) {

        if (newLocation == null) {
            return;
        }
        System.out.println(" ########## Send Current location ############# ");
        int status = -1;
        if (isBetterLocation(newLocation, currentLocation)) {
            currentLocation = newLocation;
            status = 1;
        } else {
            Location lastKnownLocation = getLastKnownLocation();
            if (isBetterLocation(lastKnownLocation, currentLocation)) {
                currentLocation = lastKnownLocation;
                status = 1;
            }
        }

        if (status == 1 && currentLocation != null) {
            this.notifyObservers(UPDATE_CURRENT_LOCATION);
            return;
        }

        MessageUtil.logMessage(newLocation.getProvider() + " location is NOT good.", true);
    }

    // API to get current location (If not using Preference Manager)
    public Location getCurrentLocation() {
        return currentLocation;
    }

    public String getCurrentLatitude() {
        return currentLocation == null ? null : String.valueOf(currentLocation.getLatitude());
    }

    public String getCurrentLongitude() {
        return currentLocation == null ? null : String.valueOf(currentLocation.getLongitude());
    }

    public void setCacheLocation(Location location) {
        if (location != null) {
            System.out.println(" ######Found and use cache location: " + location.getLatitude() + "," + location.getLongitude());
            currentLocation = location;
            if (currentLocation != null) {
                this.notifyObservers(UPDATE_CURRENT_LOCATION);
            }
        }
    }


//  #############################################################################


    //  #############################################################################
//  #                            GpsProviderListener                            #
//  #############################################################################
    private class GpsProviderListener implements LocationListener {
        public void onLocationChanged(Location newLocation) {
            updateGpsLocation(newLocation);
        }

        public void onProviderEnabled(String provider) {
            Log.v(DEBUG_TAG, "onProviderEnabled: " + provider);
        }

        public void onProviderDisabled(String provider) {
            Log.v(DEBUG_TAG, "onProviderDisabled: " + provider);
        }

        public void onStatusChanged(String provider, int status, Bundle extras) {
            switch (status) {
                case LocationProvider.AVAILABLE: {
                    Log.v(DEBUG_TAG, "LocationProvider.AVAILABLE: " + provider);
                }
                break;

                case LocationProvider.OUT_OF_SERVICE: {
                    Log.v(DEBUG_TAG, "LocationProvider.OUT_OF_SERVICE: " + provider);
                }
                break;

                case LocationProvider.TEMPORARILY_UNAVAILABLE: {
                    Log.v(DEBUG_TAG, "LocationProvider.TEMPORARILY_UNAVAILABLE: " + provider);
                }
                break;
            }
        }
    }
//  #############################################################################


    //  #############################################################################
//  #                          NetworkProviderListener                          #
//  #############################################################################
    private class NetworkProviderListener implements LocationListener {
        public void onLocationChanged(Location newLocation) {
        	System.out.println(" ####### NEtwork provider LOCATION callback  ############# ");
            updateNetworkLocation(newLocation);
        }

        public void onProviderEnabled(String provider) {
            Log.v(DEBUG_TAG, "onProviderEnabled: " + provider);
        }

        public void onProviderDisabled(String provider) {
            Log.v(DEBUG_TAG, "onProviderDisabled: " + provider);
        }

        public void onStatusChanged(String provider, int status, Bundle extras) {
            switch (status) {
                case LocationProvider.AVAILABLE: {
                    Log.v(DEBUG_TAG, "LocationProvider.AVAILABLE: " + provider);
                }
                break;

                case LocationProvider.OUT_OF_SERVICE: {
                    Log.v(DEBUG_TAG, "LocationProvider.OUT_OF_SERVICE: " + provider);
                }
                break;

                case LocationProvider.TEMPORARILY_UNAVAILABLE: {
                    Log.v(DEBUG_TAG, "LocationProvider.TEMPORARILY_UNAVAILABLE: " + provider);
                }
                break;
            }
        }
    }
//  #############################################################################


    //  ############################################################################
//  #                              TimeoutControl                              #
//  ############################################################################
    private class GpsTimer extends CountDownTimer {
        public GpsTimer(long millisInFuture, long countDownInterval) {
            super(millisInFuture, countDownInterval);
        }

        public void onFinish() {
            Log.v("GpsTimer", "onFinish()");

            if (gpsTimerActive) {
                System.out.println(" ######@@Timeout - stop receiving gps location updates");
                removeGpsLocationUpdates();
            }
        }

        public void onTick(long millisUntilFinished) {
        }
    }

    private class NetworkTimer extends CountDownTimer {
        public NetworkTimer(long millisInFuture, long countDownInterval) {
            super(millisInFuture, countDownInterval);
        }

        public void onFinish() {
            System.out.println(" ###########  NetworkTimer Finish ######### ");

            if (networkTimerActive) {
                System.out.println(" ######@@ Timeout - stop receiving network location updates");
                removeNetworkLocationUpdates();
            }
        }

        public void onTick(long millisUntilFinished) {
        }
    }

    private void startGpsTimer() {
        gpsTimer = new GpsTimer(TIME_OUT, 1000L);
        gpsTimer.start();
        gpsTimerActive = true;
    }

    private void stopGpsTimer() {
        try {
            if (gpsTimerActive == true) {
                gpsTimer.cancel();
                gpsTimerActive = false;
            }
        } catch (Exception e) {
        	e.printStackTrace();
        }
    }

    private void startNetworkTimer() {
        networkTimer = new NetworkTimer(TIME_OUT, 1000L);
        networkTimer.start();
        networkTimerActive = true;
    }

    private void stopNetworkTimer() {
        try {
            if (networkTimerActive == true) {
                networkTimer.cancel();
                networkTimerActive = false;
            }
        } catch (Exception e) {
             e.printStackTrace();
        }
    }

//  ############################################################################
}
